import logging
from abc import ABC, abstractmethod
from typing import Dict, Iterable, List, Optional, Sequence, Tuple, TypeVar

from chernc.analyzer.semantic_piece import DeclarationPiece, FunctionPiece, MacroPiece, MatchData, SemanticPiece, TypedefPiece
from chernc.analyzer.utils import (
    NoSuchChild,
    NoText,
    RustNode,
    match_similar_lower_str,
    unsafe_child_by_name,
    unsafe_child_text,
    unsafe_text,
)
from chernc.constants import DEFAULT_LOGGER_DIR, DEFAULT_LOGGER_FILE_NAME
from chernc.logging import build_logger

# since it is target based, it does not manage slice relation
# rather, it only manages source-target relation
T = TypeVar("T", bound=SemanticPiece)
logger: logging.Logger = build_logger(
    __name__ + ".semanticpiece", logger_filename=DEFAULT_LOGGER_FILE_NAME, logger_dir=DEFAULT_LOGGER_DIR
)


class TargetBasedPiece(ABC):

    @property
    @abstractmethod
    def kind(self) -> str:
        raise NotImplementedError("The kind method in the TargetBasedPiece is abstract.")

    @property
    @abstractmethod
    def text(self) -> str:
        raise NotImplementedError("The text method in the TargetBasedPiece is abstract.")

    @property
    @abstractmethod
    def matched_names(self) -> List[str]:
        raise NotImplementedError("The matched_text method in the TargetBasedPiece is abstract.")


class SingleTargetBP(TargetBasedPiece):

    @property
    @abstractmethod
    def kind(self) -> str:
        raise NotImplementedError("The kind method in the TargetBasedPiece is abstract.")

    @property
    @abstractmethod
    def text(self) -> str:
        raise NotImplementedError("The text method in the TargetBasedPiece is abstract.")


class ImplPiece(SingleTargetBP):
    method_pieces: List[FunctionPiece]
    constant_pieces: List[DeclarationPiece | MacroPiece]
    type_pieces: List[TypedefPiece]

    @staticmethod
    def extract_from_rust_nodes(nodes: Iterable[RustNode]) -> List[RustNode]:
        impls = [item for item in nodes if item.type == "impl_item"]
        return impls

    @classmethod
    def mk_fail_description(cls, node):
        """The impl node has no matched source"""
        _node_text = node.text.decode("utf-8")
        return f"the node {_node_text} has no matched source"

    # if gather fails, make llm translate again
    # pieces: collection of semantic pieces
    # node: an impl related rust node
    # return: an impl pieces
    @staticmethod
    def match_piece_for_node(pieces: Iterable[SemanticPiece], node: RustNode) -> Tuple[Optional[MatchData], List[SemanticPiece]]:
        constant_nodes: List[RustNode] = []
        function_nodes: List[RustNode] = []
        type_nodes: List[RustNode] = []
        pieces = list(pieces)
        try:
            body_node = unsafe_child_by_name(node, "body")
            impl_type_name = unsafe_child_text(node, "type")
        except NoSuchChild | NoText:
            return None, []
        item: RustNode
        for item in body_node.children:  # type: ignore
            if item.type == "{" or item.type == "}":
                continue
            elif item.type == "line_comment" or item.type == "block_comment":
                continue
            elif item.type == "const_item":
                constant_nodes.append(item)
            elif item.type == "function_item":
                function_nodes.append(item)
            elif item.type == "type_item":
                type_nodes.append(item)
            else:
                logger.error(f"The impl node contains something I dont know")

        def get_pieces(nodes: List[RustNode], candidates: List[T]) -> Tuple[Dict[RustNode, T], List[str]]:
            nonlocal impl_type_name
            pieces = {}
            names = []
            for n in nodes:
                try:
                    name = unsafe_child_text(n, "name")
                    sim_name = impl_type_name + name
                except NoSuchChild | NoText:
                    continue
                names.append(name)
                for c in candidates:
                    if c.match_similar_name(name, threshold=85) or match_similar_lower_str(c.name, sim_name, threshold=85):
                        # matched
                        pieces[n] = c
                        # candidates.remove(c)
                        break
            return pieces, names

        meth_pieces, meth_names = get_pieces(function_nodes, [p for p in pieces if isinstance(p, FunctionPiece)])
        const_pieces, const_names = get_pieces(
            constant_nodes, [p for p in pieces if isinstance(p, DeclarationPiece) or isinstance(p, MacroPiece)]
        )
        type_pieces, type_names = get_pieces(type_nodes, [p for p in pieces if isinstance(p, TypedefPiece)])
        if not meth_pieces and not const_pieces and not type_pieces:
            return None, []

        matched_nodes = meth_pieces.keys() | const_pieces.keys() | type_pieces.keys()

        try:
            text = unsafe_text(node)
        except NoText:
            return None, []

        # remove text that doesn't match
        for n in function_nodes + constant_nodes + type_nodes:
            if n in matched_nodes:
                continue
            try:
                text = text.replace(unsafe_text(n), "")
            except NoText:
                continue

        to_remove: Sequence = list(meth_pieces.values()) + list(const_pieces.values()) + list(type_pieces.values())
        try:
            return MatchData([node], [], text), to_remove
        except NoText:
            return None, []

    # constants, type, methods
    def __init__(self, method_pieces, constant_pieces, type_pieces):
        self.method_pieces = method_pieces
        self.constant_pieces = constant_pieces
        self.type_pieces = type_pieces

    @property
    def text(self) -> str:
        texts = []
        for p in self.method_pieces + self.constant_pieces + self.type_pieces:
            texts.append(p.text)
        return "\n".join(texts)

    @property
    def kind(self) -> str:
        return "impl"
